////////////////////////////////////////////////////////////////////////////////
//
//	This file is part of Woozox, see <http://code.google.com/p/woozox/>.
//
//	Copyright 2008-2009 Florian David
//
//	Woozoo is free software: you can redistribute it and/or modify
//	it under the terms of the GNU Lesser General Public License as 
//	published by the Free Software Foundation, either version 3 of 
//	the License, or (at your option) any later version.
//
//	Woozoo is distributed in the hope that it will be useful,
//	but WITHOUT ANY WARRANTY; without even the implied warranty of
//	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//	GNU General Public License for more details.
//
//	You should have received a copy of the GNU Lesser General Public 
//	License along with Woozoo. If not, see <http://www.gnu.org/licenses/>.
//
////////////////////////////////////////////////////////////////////////////////
package org.woozoo.xhtml.core.base
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	
	import org.woozoo.xhtml.core.manager.element.ElementManager;
	import org.woozoo.xhtml.core.util.UIDUtil;
	
	/**
	 * This is the class that represent an XHTML node
	 * */
	public class Element extends EventDispatcher
	{
		// if you add a property, take a look at the __init() method
		private var __source : XML = null;
		private var __innerHTML : String = null;
		private var __tagName : String = null;
		private var __isValid : Boolean = false;
		private var __document : Document = null;
		private var __childIndex : int = -1;
		private var __children : Array = null;
		private var __childrenAndSoOn : Array = null;
		private var __firstChild : Element = null;
		private var __lastChild : Element = null;
		private var __numChildren : int = -1;
		private var __parent : Element = null;
		private var __attributesCached : Object = {};
		private var __wzxInternalUID : String;
		
		//////////////////////////////////////////////////////
		//					CONSTRUCTOR						//
		//////////////////////////////////////////////////////
		
		/**
		 * Constructor.
		 * 
		 * @param source The xml node that represent the element (could be either a String, a XML or an Element)
		 * */
		public function Element(document : Document, source : *) {
			this.source = source;
			__document = document;
		}

		//////////////////////////////////////////////////////
		//						PUBLIC						//
		//////////////////////////////////////////////////////

		public function get wzxInternalUID() : String {
			var uid : String = __source.@wzx_internal_uid;
			if (!uid) {
				uid = __source.@wzx_internal_uid = UIDUtil.createUID();
			}
			return uid;
		}
		
		/**
		 * Get the document associated to this element
		 * */
		public function get document() : Document {
			return __document;
		}
		
		/**
		 * Return the parent element of this element
		 * */
		public function get parent() : Element {
			if (this is Document) return null;
			if (!__parent) {
				__parent = ElementManager.getInstance().create(document, __source.parent()); 
			}
			return __parent;
		}
		
		/**
		 * Return the siblingIndex of this element related to its direct parent (-1 if no parent)
		 * */
		public function get siblingIndex() : int {
			if (__childIndex == -1) {
				__childIndex = __source.childIndex();
			}
			return __childIndex;
		}
		
		/**
		 * TODO Buffer
		 * */
		public function get isLastSibling() : Boolean {
			if (!parent) return true;
			return siblingIndex == parent.numChildren - 1;
		}

		/**
		 * TODO Buffer
		 * */
		public function get isFirstSibling() : Boolean {
			if (!parent) return true;
			return siblingIndex == 0;
		}
		
		/**
		 * TODO Buffer
		 * */
		public function get previousSibling() : Element {
			var element : Element;
			if (!isFirstSibling) {
				element = parent.getChildAt(siblingIndex - 1);
			}
			return element;
		}
		
		/**
		 * TODO Buffer
		 * */
		public function get nextSibling() : Element {
			var element : Element
			if (!isLastSibling) {
				element = parent.getChildAt(siblingIndex + 1);
			}
			return element;
		}
		
		/**
		 * Will return the source of this element has a String using XML::toString().
		 * 
		 * @return The string
		 * */
		override public function toString() : String {
			if (!__source)
				return "";
			return __source.toString();
		}
		
		/**
		 * Compare this element to the parameter by reference
		 * 
		 * @param element The element to compare
		 * @return Are they equal ?
		 * */
		public function isEqualTo(element : Element) : Boolean {
			return element == this;
		}
		
		/**
		 * Will return if the source of this Element is valid XML.
		 * 
		 * @return Is the source valid XML
		 * */
		public function get isValid() : Boolean {
			return __isValid;
		}
		
		/**
		 * The source XML of this element.
		 * 
		 * @return The XML
		 * */
		public function get source() : XML {
			return __source;
		} 
		
		/**
		 * Set the source (xml) of this element.
		 * 
		 * <p>The value could either be a XML, an Element or a String</p>
		 * */
		public function set source(value : *) : void {
			__init();
			try {
				switch (true) {
					case (value is XML):
						__source = value;
					break;
					case (value is String):
						__source = XML(value);
					break;	
					case (value is Element):
						__source = (value as Element).source;
					break;
				}
				__isValid = true;
				dispatchEvent(new Event('sourceChange'));
			} catch (error : Error) {
				trace('Element : '+error.message)
				__isValid = false;
			}
		}
		
		/**
		 * Get the value of an attribute (proxy to getAttribute).
		 * 
		 * <p>For example, your source is <code>&lt;div title="myTitle"&gt;&lt;a&gt;myLabel&lt;/a&gt;&lt;/div&gt;</code>,<br />
		 * <code>att('title')</code> will return the string <code>myTitle</code></p>
		 * 
		 * @param str The name of the attribute 
		 * @return The value of the attribute
		 * */
		public function att(str : String) : String {
			return getAttribute(str);
		}
		
		/**
		 * Test if the attribute exist on this element.
		 * 
		 * @param str The attibute to test
		 * @return Did we find it ?
		 * */
		public function hasAttribute(str : String) : Boolean {
			return getAttribute(str) != "";
		}
		
		/**
		 * Test if the value of the attribute str is equal to value
		 * 
		 * @param str The attribute to test 
		 * @param value The value
		 * @return Is it equal ?
		 * */
		public function hasAttributeEqualTo(str : String, value : String) : Boolean {
			return getAttribute(str) == value;
		}

		/**
		 * Test if value is in the value of the attribute str
		 * 
		 * @param str The attribute to test 
		 * @param value The value
		 * @return 
		 * */
		public function hasAttributeContains(str : String, value : String) : Boolean {
			return getAttribute(str).indexOf(value) >= 0;
		}
		
		/**
		 * Test if the value of the attribute str start with value
		 * 
		 * @param str The attribute to test 
		 * @param value The value
		 * @return 
		 * */
		public function hasAttributeStartWith(str : String, value : String) : Boolean {
			return getAttribute(str).substr(0, value.length) === value;
		}
		
		/**
		 * Test if the value of the attribute str end with value
		 * 
		 * @param str The attribute to test 
		 * @param value The value
		 * @return 
		 * */
		public function hasAttributeEndWith(str : String, value : String) : Boolean {
			var att : String = getAttribute(str);
			var attLen : int = att.length;
			var valueLen : int = value.length;
			return att.substr(attLen - valueLen, attLen) === value;
		}
		
		/**
		 * 
		 * */
		public function hasValueInAttributeWhitespaceSeparated(att : String, value : String) : Boolean {
			return __hasValueInAttributeSeparated(' ', att, value);
		}
		
		/**
		 * 
		 * */
		public function hasValueInAttributeHyphenSeparated(att : String, value : String) : Boolean {
			return __hasValueInAttributeSeparated('-', att, value);
		}
		
		/**
		 * Return the xhtml id of the element
		 * */
		public function get id() : String {
			return getAttribute('id')
		}

		/**
		 * Return the xhtml class of the element
		 * */
		public function get className() : String {
			return getAttribute('class');
		}
		
		/**
		 * Return the direct children of this element
		 * */
		public function get children() : Array {
			if (!__children) {
				__children = [];
				for (var i : int = 0; i < numChildren; i++) {
					var child : Element = getChildAt(i);
					__children.push(child);
				}
			}
			return __children;
		}

		/**
		 * Return children, grand children and so on of this element
		 * */
		public function get childrenAndSoOn() : Array {
			if (!__childrenAndSoOn) {
				__childrenAndSoOn = [];
				for (var i : int = 0; i < numChildren; i++) {
					var child : Element = getChildAt(i);
					__childrenAndSoOn = __childrenAndSoOn.concat(child, child.childrenAndSoOn);
				}
			}
			return __childrenAndSoOn;
		}
		
		/**
		 * Get the value of an attribute.
		 * 
		 * @param name The name of the attribute 
		 * @return The value of the attribute
		 * @see #att()
		 * */ 
		public function getAttribute(name : String) : String {
			return __getAttributeAndCache(name);
		}
		
		/**
		 * The innerHTML part of the element.
		 * 
		 * <p>For example, your source is &lt;div&gt;&lt;a&gt;myLabel&lt;/a&gt;&lt;/div&gt;,
		 * innerHTML will return the string &lt;a&gt;myLabel&lt;/a&gt;</p>
		 * 
		 * @return The innerHTML
		 * */
		public function get innerHTML() : String {
			if (__innerHTML == null) {
				__innerHTML = __source.children().toXMLString();
			}
			return __innerHTML;
		}
		
		/**
		 * Will return true if the element has no children at all.
		 * 
		 * <p>Will return true for <code>&lt;p&gt;&lt;/p&gt;</code> but false for <code>&lt;p&gt;&lt;a&gt;&lt;/a&gt;&lt;/p&gt;</code> 
		 * or <code>&lt;p&gt;hello&lt;/p&gt;</code></p>
		 * <p>This getter is used by selector <code>:empty</code>.</p>
		 * */
		public function get isEmpty() : Boolean {
			return innerHTML == "";
		}
		
		/**
		 * The tagName of the element.
		 * 
		 * <p>For example, your source is <code>&lt;div title="myTitle"&gt;&lt;a&gt;myLabel&lt;/a&gt;&lt;/div&gt;</code>,<br />
		 * <code>tagName</code> will return the string <code>div</code></p>
		 * <p>Note that the tagName is always in lower case.</p>
		 * 
		 * @return The tagName
		 * */
		public function get tagName() : String {
			if (__tagName == null) {
				var localName : String = __source.localName();
				if (localName) {
					__tagName = localName.toString().toLowerCase();
				} else {
					__tagName = "";
				}
			}
			return __tagName;
		}
		
		/**
		 * Return the first child of this element.
		 * */
		public function get firstChild() : Element {
			if (!__firstChild) {
				__firstChild = getChildAt(0);
			}
			return __firstChild;
		}

		/**
		 * Return the first child of this element.
		 * */
		public function getFirstChildOfType(type : String) : Element {
			return getElementsByTagName(type, 1, 1)[0];
		}
		
		/**
		 * Return the child element of this element at the specified index
		 * 
		 * @param index The index of the child
		 * @return The finded child
		 * */
		public function getChildAt(index : int) : Element {
			return ElementManager.getInstance().create(document, __source.children()[index]);
		}

		/**
		 * Return the last child of this element.
		 * */
		public function get lastChild() : Element {
			if (!__lastChild) {
				__lastChild = getChildAt(numChildren - 1);
			}
			return __lastChild;
		}
		
		/**
		 * Return the number of direct children
		 * */
		public function get numChildren() : int {
			if (__numChildren == -1) {
				__numChildren = __source.children().length();
			}
			return __numChildren;
		}
		
		/**
		 * Will return true if you Element has the specified class.
		 * 
		 * <p>For example, your source is <code>&lt;div class="my-class blue"&gt;&lt;a&gt;myLabel&lt;/a&gt;&lt;/div&gt;</code>,<br />
		 * <code>hasClass('my-class')</code> will return <code>true</code><br />
		 * <code>hasClass('blue')</code> will return <code>true</code><br />
		 * <code>hasClass('red')</code> will return <code>false</code></p>
		 * 
		 * @param className The name of the class to test
		 * @return Does the element has this class
		 * */
		public function hasClass(className : String) : Boolean {
			return hasValueInAttributeWhitespaceSeparated('class', className);
		}
		
		
		/**
		 * Get the list of the children (Element) that match the tag.
		 * 
		 * <p>This method is similar to the document.getElementsByTagName() javascript method.</p>
		 * 
		 * @param tagName The tag you are looking for
		 * @param maxDepth How deep in children you want to go (-1 is infinite)
		 * @param maxNumber Number of max element you are looking for (-1 is infinite)
		 * @return the list of Elements matching
		 * */
		public function getElementsByTagName(tagName : String, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements("", tagName.toLowerCase(), __getElementsByTagNameTestFunction, __source.children(), maxDepth, maxNumber);
		}
		
		/**
		 * Get the list of the children that match the className
		 * 
		 * @param className The class you are looking for
		 * @param maxDepth How deep in children you want to go (-1 is infinite)
		 * @param maxNumber Number of max element you are looking for (-1 is infinite)
		 * @return the list of Elements matching
		 * */
		public function getElementsByClassName(className : String, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements('class', className, __getElementsByClassNameTestFunction, __source.children(), maxDepth, maxNumber);
		}
		
		/**
		 * Get the list of the children that has the attribute 'attributeName' equals to 'attributeValue'
		 * 
		 * @param attributeName The name of the attribute
		 * @param attributeValue The value that the content of the attibute should match
		 * @param maxDepth How deep in children you want to go (-1 is infinite)
		 * @param maxNumber Number of max element you are looking for (-1 is infinite)
		 * @return the list of Elements matching
		 * */
		public function getElementsByAttributeEqualTo(attributeName : String, attributeValue : *, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements(attributeName, attributeValue, __getElementsByAttributeEqualToTestFunction, __source.children(), maxDepth, maxNumber);
		}

		/**
		 * Get the list of the children that has the attribute 'attributeName' start with 'attributeValue'
		 * 
		 * @param attributeName The name of the attribute
		 * @param attributeValue The value that the content of the attibute should match
		 * @param maxDepth How deep in children you want to go (-1 is infinite)
		 * @param maxNumber Number of max element you are looking for (-1 is infinite)
		 * @return the list of Elements matching
		 * */
		public function getElementsByAttributeStartWith(attributeName : String, attributeValue : *, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements(attributeName, attributeValue, __getElementsByAttributeStartWithTestFunction, __source.children(), maxDepth, maxNumber);
		}

		/**
		 * Get the list of the children that has the attribute 'attributeName' end with 'attributeValue'
		 * 
		 * @param attributeName The name of the attribute
		 * @param attributeValue The value that the content of the attibute should match
		 * @param maxDepth How deep in children you want to go (-1 is infinite)
		 * @param maxNumber Number of max element you are looking for (-1 is infinite)
		 * @return the list of Elements matching
		 * */
		public function getElementsByAttributeEndWith(attributeName : String, attributeValue : *, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements(attributeName, attributeValue, __getElementsByAttributeEndWithTestFunction, __source.children(), maxDepth, maxNumber);
		}

		/**
		 * 
		 * */
		public function getElementsByAttributeWhitespaceSeparated(attributeName : String, attributeValue : *, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements(attributeName, attributeValue, __getElementsByAttributeWhitespaceSeparatedTestFunction, __source.children(), maxDepth, maxNumber);
		}

		/**
		 * 
		 * */
		public function getElementsByAttributeHyphenSeparated(attributeName : String, attributeValue : *, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements(attributeName, attributeValue, __getElementsByAttributeHyphenSeparatedTestFunction, __source.children(), maxDepth, maxNumber);
		}

		/**
		 * 
		 * */
		public function getElementsByAttributeContains(attributeName : String, attributeValue : *, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements(attributeName, attributeValue, __getElementsByAttributeContainsTestFunction, __source.children(), maxDepth, maxNumber);
		}
		
		/**
		 * Get the list of the children that has the attribute 'attributeName'
		 * 
		 * @param attributeName The name of the attribute
		 * @param maxDepth How deep in children you want to go (-1 is infinite)
		 * @param maxNumber Number of max element you are looking for (-1 is infinite)
		 * @return the list of Elements matching
		 * */
		public function getElementsHasAttribute(attributeName : String, maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements(attributeName, "", __getElementsHasAttributeTestFunction, __source.children(), maxDepth, maxNumber);
		}
		
		public function getElementsEmpty(maxDepth : int = -1, maxNumber : int = -1) : Array /* of Element */ {
			if (!__source) return [];
			return __getElements("", "", __getElementsEmptyTestFunction, __source.children(), maxDepth, maxNumber);
		}
		
		//////////////////////////////////////////////////////
		//						PRIVATE						//
		//////////////////////////////////////////////////////
		
		/**
		 * 
		 * */
		private function __init() : void {
			__source = null;
			__innerHTML = null;
			__tagName = null;
			__isValid = false;
			__childIndex = -1;
			__children = null;
			__childrenAndSoOn = null;	
			__firstChild = null;	
			__lastChild = null;
			__numChildren = -1;	
			__parent = null;
			__attributesCached = {};
		}

		private function __getAttributeAndCache(name : String) : String {
			if (!__attributesCached[name]) {
				__attributesCached[name] = __source.@[name];
			}
			return __attributesCached[name];
		}
		
		/**
		 * 
		 * */
		private function __getElements(testKey : String, testValue : *, callbackTest : Function, children : XMLList, maxDepth : int = -1, depth : int = 0, maxNumber : int = -1) : Array /* of Element */ {
			var list : Array = [];
			var childrenLen : int = children.length();
			for (var  i : int = 0; i < childrenLen; i++) {
				var child : XML = children[i];
				var element : Element = callbackTest(testKey, testValue, child);
				if (element) {
					list.push(element);
				}
				if (maxNumber != -1 && list.length >= maxNumber) {
					break;
				} 
				if (child.hasComplexContent() && (maxDepth == -1 || depth + 1 < maxDepth)) {
					list = list.concat(__getElements(testKey, testValue, callbackTest, child.children(), maxDepth, depth + 1));
				}
			}
			return list;
		}
		
		private function __getElementsHasAttributeTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			var xmlAttr : String = xml.@[testKey].toString();
			if (xmlAttr != "") {
				return ElementManager.getInstance().create(document, xml);
			}
			return null;
		}
		
		private function __getElementsByClassNameTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			var element : Element = ElementManager.getInstance().create(document, xml);
			if (element.hasClass(testValue)) {
				return element;
			}
			return null;
		}
		
		private function __getElementsByTagNameTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			var name : String = xml.localName();
			if ((testValue == "*" && name) || name == testValue) {
				return ElementManager.getInstance().create(document, xml);
			}
			return null;
		}
		
		private function __hasValueInAttributeSeparated(separator : String, att : String, value : String) : Boolean {
			var lAtt : String = '"'+getAttribute(att)+'"';
			var pattern : RegExp = new RegExp('["|'+separator+']'+value+'["|'+separator+']');
			var res : Array = lAtt.match(pattern);
			return res && res.length > 0;
		}
		
		
		private function __getElementsByAttributeEqualToTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			var xmlAttr : String = xml.@[testKey].toString();
			if (xmlAttr == testValue) {
				return ElementManager.getInstance().create(document, xml);
			}
			return null;
		}
		
		private function __getElementsEmptyTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			var element : Element = ElementManager.getInstance().create(document, xml);
			if (element.isEmpty) {
				return element;
			}
			return null;
		}
		
		private function __getElementsByAttributeStartWithTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			return __getElementsByAttributeSomethingTestFunction('hasAttributeStartWith', testKey, testValue, xml);
		}

		private function __getElementsByAttributeEndWithTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			return __getElementsByAttributeSomethingTestFunction('hasAttributeEndWith', testKey, testValue, xml);
		}

		private function __getElementsByAttributeContainsTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			return __getElementsByAttributeSomethingTestFunction('hasAttributeContains', testKey, testValue, xml);
		}

		private function __getElementsByAttributeWhitespaceSeparatedTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			return __getElementsByAttributeSomethingTestFunction('hasValueInAttributeWhitespaceSeparated', testKey, testValue, xml);
		}
	
		private function __getElementsByAttributeHyphenSeparatedTestFunction(testKey : String, testValue : String, xml : XML) : Element {
			return __getElementsByAttributeSomethingTestFunction('hasValueInAttributeHyphenSeparated', testKey, testValue, xml);
		}
		
		private function __getElementsByAttributeSomethingTestFunction(methodName : String, testKey : String, testValue : String, xml : XML) : Element {
			var element : Element = ElementManager.getInstance().create(document, xml);
			if (element[methodName](testKey, testValue)) {
				return element;
			}
			return null;
		}
	}
}